﻿Option Explicit On

Imports System.IO

''' <summary>
''' The PageList class implements a circular buffer of page codes to be displayed.
''' The page codes are read from a target source file and loaded into the buffer or list.
''' The List is circular in nature in that repeated calls to NextItem() will continue to
''' wrap around the end of the internal list and start again at the beginning.
''' 
''' The class has two primary responsibilites.  First is to manage a list of active and 
''' valid page codes seeded from the specified source file.  The second is to monitor changes 
''' to the source file and reload its contents so that the internal list of codes matches the 
''' source file.
''' 
''' When reading the source file the class parses the contents looking for valid page codes.
''' Valid page codes are found one per line and have exactly 3 chars with no spaces. 
''' Lines beginning with a '#' character are treated as comments and ignored. 
''' 
''' The class uses a .NET FileSystemWatcher background task to monitor and event on any
''' changes to the source file.  After the initial seeding, the source file is only re-read
''' when the FileSystemWatcher reports a change to the file.
''' </summary>
''' <remarks>
''' Error Code Management
''' This class uses an Error Bit String to record and report on possibly multiple errors.
''' Each error is given its own bit postion in the 32-bit Integer Error code.  The first
''' lower 16-bits (0-15, or 2^0 - 2^15) are reserved for Page Code errors resulting from
''' parse errors.  The last higher 16 bits (16-31, or 2^16 - 2^31) are used for more serious
''' issues such as file read errors.
''' 
''' The reason for using the bit string is that multiple errors may be found during the
''' reading and parsing of the source file.  Instead of firing off an error event for every
''' issue the issues are collected and then a single error event is fired when the reading
''' of the source file has completed.
''' </remarks>

Public Class PageList

    ' -------------------------------------------------------------------------
    ' Constants
    ' -------------------------------------------------------------------------

    ' Defined Error Codes and Bit Positions
    Public Const ERR_NONE = 0
    'Page Code Errors 
    Public Const ERR_BAD_CODE = 2 ^ 1
    'Page File Errors
    Public Const ERR_FILE_MISSING = 2 ^ 16
    Public Const ERR_FILE_READ_ERROR = 2 ^ 17

    ' -------------------------------------------------------------------------
    ' Member Variables
    ' -------------------------------------------------------------------------
    Private mData As ArrayList
    Private mBadCodes As ArrayList
    Private mIndex As Integer
    Private mSourceFile As String
    Private mFileWatcher As FileSystemWatcher
    Private mLastEventTime As Date
    Private mPageError As Integer = ERR_NONE
    Private mErrorMessages As String = ""


    ' -------------------------------------------------------------------------
    ' Property Definitions
    ' -------------------------------------------------------------------------
    '
    Public ReadOnly Property SourceFile() As String
        Get
            Return mSourceFile
        End Get
    End Property

    Public ReadOnly Property ErrorCode() As Integer
        Get
            Return mPageError
        End Get
    End Property

    Public ReadOnly Property ErrorMessages() As String
        Get
            Return mErrorMessages
        End Get
    End Property

    ''' <summary>
    ''' Set to TRUE to enable the PageList object to monitor and re-read the source file
    ''' whenever it changes.  Otherwise the method Refresh() must be manually called.
    ''' </summary>
    Public WriteOnly Property EnableAutoRefresh As Boolean
        Set(ByVal value As Boolean)
            mFileWatcher.EnableRaisingEvents = value
        End Set
    End Property


    ' -------------------------------------------------------------------------
    ' Event Definitions
    ' -------------------------------------------------------------------------
    '
    Public Event Changed(ByVal sender As PageList, ByVal e As EventArgs)

    Protected Overridable Sub OnChanged(ByVal e As EventArgs)
        RaiseEvent Changed(Me, e)
    End Sub

    Public Event ErrorEvent(ByVal sender As PageList, ByVal e As PageListEventArgs)

    Protected Overridable Sub OnErrorEvent(ByVal e As EventArgs)
        RaiseEvent ErrorEvent(Me, e)
    End Sub


    ' -------------------------------------------------------------------------
    ' Main Code
    ' -------------------------------------------------------------------------

    ''' <summary>
    ''' Constructor. Requires name of source file to seed Page Codes from.
    ''' </summary>
    ''' <param name="sourceFile">Full path of target source file.</param>
    ''' <remarks>Enables immediate reading of source file to seed list.</remarks>
    Public Sub New(ByVal sourceFile As String)
        Me.New(sourceFile, True)
    End Sub

    ''' <summary>
    ''' Constructor. Requires name of source file to seed Page Codes from and 
    ''' a boolean value indicating if page list should be loaded immediately from
    ''' the source file.
    ''' </summary>
    ''' <param name="sourceFile">Full path of target source file.</param>
    ''' <param name="loadList">True to immediately load page list from file. False
    ''' to defer initial loading until the Refresh() method is called.</param>
    ''' <remarks></remarks>
    Public Sub New(ByVal sourceFile As String, ByVal loadList As Boolean)
        If sourceFile <> Nothing And sourceFile.Length > 0 Then
            mSourceFile = sourceFile
            ' Create Thread Safe collection elements
            mData = ArrayList.Synchronized(New ArrayList())
            mBadCodes = ArrayList.Synchronized(New ArrayList())
            mIndex = 0

            setupFileWatch()

            If loadList Then
                refresh()
            End If
        Else
            Throw (New ArgumentException("Invalid argument. Source file name required."))
        End If
    End Sub

    Public Function hasErrors() As Boolean
        Return Me.ErrorCode <> ERR_NONE
    End Function

    ''' <summary>
    ''' A helper method to determine if the current error string has any Page Code Errors
    ''' </summary>
    ''' <returns>True if it does. otherwise False.</returns>
    Public Function hasCodeErrors() As Boolean
        Return Me.ErrorCode And ((2 ^ 16) - 1)
    End Function

    ''' <summary>
    ''' A helper method to determine if the current error string has any File I/O Errors
    ''' </summary>
    ''' <returns>True if it does. otherwise False.</returns>
    Public Function hasFileErrors() As Boolean
        Return Me.ErrorCode >= (2 ^ 16)
    End Function

    ''' <summary>
    ''' Returns the contents of the Page List as a String array.
    ''' </summary>
    Public Function getList() As String()
        Return DirectCast(mData.ToArray(GetType(String)), String())
    End Function

    ''' <summary>
    ''' Returns number of individual page codes in the current list.
    ''' </summary>
    Public Function getCount() As Integer
        Return mData.Count
    End Function

    ''' <summary>
    ''' Main method to refresh the Page List contents by re-reading the source file.
    ''' This method can be called at any time.  Optionally if the Property EnableAutoRefresh
    ''' is set to True then this method is invoked automatically by the PageList object
    ''' itself anytime a change to the sourcefile is detected.
    ''' </summary>
    ''' <returns>True if new Page List content differs from previous content (prior to refresh),
    ''' otherwise, False.</returns>
    ''' <remarks>This method will trigger Error Events if nay errors are encountered during
    ''' the refresh. </remarks>
    Public Function refresh() As Boolean

        Dim strNextCode As String
        Dim strOldData() As String
        Dim intIndex As Integer
        Dim prevErrors As Integer = mPageError

        'Reset Error codes
        mPageError = ERR_NONE
        mErrorMessages = ""
        mBadCodes.Clear()

        ' Find what the next code would be the current list
        strNextCode = Me.peek()

        strOldData = Me.getList()

        ' Create a new list from source file
        createListFromFile(mSourceFile)

        ' Find index for the next code (if still active) so we don't end up skipping over it.
        If Not strNextCode = vbNullString Then
            intIndex = mData.IndexOf(strNextCode)
            If intIndex > -1 Then
                mIndex = intIndex
            End If
        End If

        Dim e = New PageListEventArgs(Me.ErrorCode, Me.ErrorMessages)
        If hasErrors() Then
            OnErrorEvent(e)
        ElseIf prevErrors > ERR_NONE Then
            ' Previous errors have cleared, send clear event
            OnErrorEvent(e)
        End If

        ' Has the List content changed?
        Return Not arraysAreEqual(strOldData, Me.getList())

    End Function

    Public Function isEmpty() As Boolean
        Return (mData.Count = 0)
    End Function

    ''' <summary>
    ''' Returns the next item in the Page List and advances the internal list pointer.  
    ''' As PageList implements a circular buffer, subsequent calls will wrap around the end 
    ''' of list and continue from the beginning of the list.
    ''' </summary>
    Public Function nextItem() As String

        Dim strCode As String

        strCode = Me.peek()

        If Not (strCode = "" Or strCode = vbNullString) Then
            mIndex = mIndex + 1
        End If

        Return strCode

    End Function

    ''' <summary>
    ''' Returns the next item in the list without advancing the internal list pointer.
    ''' </summary>
    Public Function peek() As String

        Dim intQSize As Integer
        Dim rv As String

        intQSize = mData.Count
        rv = vbNullString

        If intQSize > 0 Then
            If (mIndex > intQSize - 1) Then
                mIndex = 0
            End If

            rv = mData.Item(mIndex)

        End If

        Return rv

    End Function

    ''' <summary>
    ''' Returns True if specified string is contained in the Page List, otherwise False.
    ''' </summary>
    ''' <param name="strSearch">Item to look for.</param>
    Public Function contains(ByVal strSearch As String) As Boolean
        Return mData.Contains(strSearch)
    End Function

    ''' <summary>
    ''' Clears the page list contents until the nest refresh() is called.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub clear()
        mData.Clear()
    End Sub

    ''' <summary>
    ''' Returns the contents of the Page List as a string of space seperated codes.
    ''' </summary>
    Public Overrides Function toString() As String
        Return listToString(mData)
    End Function

    ''' <summary>
    ''' Returns the bad page codes read from source file as a space seperated string of codes.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function badCodesToString() As String
        Return listToString(mBadCodes)
    End Function

    Private Sub createListFromFile(ByVal FileName As String)

        Dim strLine As String
        Dim srReader As StreamReader

        SyncLock mData.SyncRoot

            clear()

            If File.Exists(FileName) Then
                Try
                    System.Threading.Thread.Sleep(500)
                    srReader = File.OpenText(FileName)
                    Do While srReader.Peek() >= 0
                        strLine = srReader.ReadLine()
                        strLine = Trim(strLine)
                        If Mid(strLine, 1, 1) <> "#" And strLine.Length > 0 Then
                            If Len(strLine) = 3 Then
                                addItem(UCase(strLine))
                            Else
                                mBadCodes.Add(strLine)
                                addError(ERR_BAD_CODE, "Bad Codes In Page Code List: " & _
                                                           badCodesToString())
                            End If
                        End If
                    Loop

                Catch ex As Exception
                    ' Problem with reading file
                    addError(ERR_FILE_READ_ERROR, "Exception occured reading Page File. Exception Text: " & ex.Message)
                Finally
                    If Not IsNothing(srReader) Then
                        srReader.Close()
                        srReader.Dispose()
                    End If
                End Try
            Else
                ' Page Code File does nto exist
                addError(ERR_FILE_MISSING, "Page File Is Missing or Inaccessible!")
            End If

        End SyncLock

    End Sub

    ''' <summary>
    ''' Add an Error Code to the internal Error Bit String.
    ''' </summary>
    ''' <param name="errCode">Error Code to add.</param>
    ''' <param name="message">Error Message associated with Code.  Multiple Error messages
    ''' are concatonated together into a single string with new line characters.</param>
    Private Sub addError(ByVal errCode As Integer, ByVal message As String)
        mPageError = mPageError Or errCode
        If mErrorMessages.Length = 0 Then
            mErrorMessages = message
        Else
            mErrorMessages &= vbNewLine & message
        End If
    End Sub

    Private Function listToString(ByRef list As ArrayList) As String
        Dim rv As String = ""
        SyncLock list.SyncRoot
            For Each code In list
                If rv = "" Then
                    rv = code
                Else
                    rv = rv & ", " & code
                End If
            Next
            Return rv
        End SyncLock
    End Function

    Private Sub fileChangedHandler(ByVal sender As Object, ByVal e As FileSystemEventArgs)
        ' Fix for the FileSystemHandler double event call bug
        If DateTime.Now.Subtract(mLastEventTime).TotalMilliseconds > 100 Then
            mLastEventTime = DateTime.Now
            Debug.WriteLine("File " + e.FullPath + " :" + e.ChangeType.ToString)

            If refresh() Then
                OnChanged(e)
            End If

        End If

    End Sub

    Private Sub setupFileWatch()
        ' Setup File Watcher
        Try
            mFileWatcher = New FileSystemWatcher()
            mFileWatcher.Path = Path.GetDirectoryName(SourceFile)
            mFileWatcher.Filter = Path.GetFileName(SourceFile)
            mFileWatcher.NotifyFilter = NotifyFilters.LastWrite Or NotifyFilters.Size Or NotifyFilters.FileName
            AddHandler mFileWatcher.Changed, AddressOf fileChangedHandler
            mFileWatcher.EnableRaisingEvents = False
        Catch ex As Exception
            Throw ex
        End Try

    End Sub

    Private Function arraysAreEqual(ByVal Array1() As String, ByVal Array2() As String) As Boolean

        Dim areEqual As Boolean = False
        Dim strVal As String

        If Array1.Length = Array2.Length Then
            areEqual = True

            For Each strVal In Array1
                If Not Array2.Contains(strVal) Then
                    areEqual = False
                    Exit For
                End If
            Next

        End If

        Return areEqual

    End Function

    Private Sub addItem(ByVal strValue As String)
        mData.Add(strValue)
    End Sub


    ' Inner Class To Handle Event Args
    Public Class PageListEventArgs
        Inherits EventArgs

        Private mErrorCode As Integer
        Private mMessage As String = ""
        Private mTime As DateTime

        Public ReadOnly Property ErrorCode As Integer
            Get
                Return mErrorCode
            End Get
        End Property

        Public ReadOnly Property Message As String
            Get
                Return mMessage
            End Get
        End Property

        Public ReadOnly Property Time As DateTime
            Get
                Return mTime
            End Get
        End Property

        Public Sub New(ByVal errCode As Integer, ByVal message As String)
            Me.New(errCode, message, DateTime.Now)
        End Sub

        Private Sub New(ByVal errCode As Integer, ByVal message As String, ByVal time As Date)
            MyBase.New()
            mErrorCode = errCode
            mMessage = message
            mTime = time
        End Sub

    End Class

End Class
